﻿#region license
// Copyright (c) 2005, Sorin Ionescu (sorin.ionescu@gmail.com)
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
// 
//     * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//     * Neither the name of Sorin Ionescu nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Taken and modified

# This file is part of Grawlix.
# 
# Grawlix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# 
# Grawlix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public
# License along with Grawlix.  If not, see
# <http://www.gnu.org/licenses/>.
#endregion

"""Implementation of the disposable pattern."""

namespace Grawlix.Meta
import Boo.Lang.Compiler
import Boo.Lang.Compiler.TypeSystem
import System
import C5
import System.Collections.Generic as SCG

transient class DisposableAttribute(AbstractAstAttribute):
"""Implements the IDisposable pattern for a type."""
	
	override def Apply(targetNode as Ast.Node):
	"""
	Applies the DisposableAttribute to the specified node.
	Param targetNode: The node to apply the attribute to.
	"""
		
		_disposableType = targetNode as Ast.TypeDefinition
		if _disposableType is null:
			InvalidNodeForAttribute('Class Definition')
			return
		
		data = DisposableData()
		if Alias is not null:
			data.Alias = _alias.Name
		else:
			data.Alias = ''
		if SharedCleanup is not null:
			data.SharedCleanup = SharedCleanup.Name
			data.WantFinalizer = true
		else:
			data.SharedCleanup = ''
			if ForceFinalizer:
				data.WantFinalizer = ForceFinalizer.Value
		data.LexicalInfo = LexicalInfo
		
		targetNode.Annotate(_disposeKey, data)
		if Context[_disposeKey] is null:
			pipeline = InnerPipeline(Context, Steps.CheckGenericConstraints)
			pipeline.Add(ImplementDisposablePattern())
			pipeline.Add(SealMethodsInFinalClasses())
			Context[_disposeKey] = pipeline
		
		CreateIDisposableDerivation()
		CreateDisposedField()
		CreateProtectedDisposeMethod(data.SharedCleanup)
		CreateDisposeMethod('Dispose')
		CreateExplicitDisposeMethod()
		if data.Alias.Length != 0:
			CreateDisposeMethod(data.Alias)
		CreateFinalizer()
		
		skipList = ArrayList[of string]()
		skipList.Add('Dispose')
		skipList.Add('Finalize')
		skipList.Add('ToString')
		if data.SharedCleanup.Length != 0:
			skipList.Add(data.SharedCleanup)
		if data.Alias.Length != 0:
			skipList.Add(data.Alias)
		CreateDisposedObjectCheck(skipList)
		CreateDisposedToString()
		
		Context.Parameters.Pipeline.AfterStep += do(
			sender as object,
			e as CompilerStepEventArgs):
			
			_context = e.Context
			
			if e.Step isa Steps.ProcessMethodBodies:
				GenerateDisposeCascade()
		
		# Need to rerun some previous steps to bind types
		#reprocess = CompilerPipeline()
		#reprocess.Add(Steps.BindTypeDefinitions())
		#reprocess.Add(Steps.BindGenericParameters())
		#reprocess.Add(Steps.BindNamespaces())
		#reprocess.Add(Steps.BindBaseTypes())
		#reprocess.Run(Context)
	
	public static def FindNodeData(node as Ast.Node) as DisposableData:
	"""
	Find the DisposableAttribute data associated with a specified node.
	Returns: The attribute data.
	"""
		if node.ContainsAnnotation(_disposeKey):
			return node[_disposeKey]
		return null
	
	protected def CreateIDisposableDerivation():
	"""Creates the IDisposable derivation."""
		reference = CodeBuilder.CreateTypeReference(IDisposable)
		_disposableType.BaseTypes.Add(reference)
	
	protected def CreateDisposedField():
	"""
	Creates the disposed flag.
	Remarks: The flag is used to check whether the
		instance has been disposed of.
	"""
		identifier = \
			Ast.AstUtil.BuildUniqueTypeMemberName(DisposableType, 'disposed')
		_disposed = Ast.AstUtil.CreateReferenceExpression(identifier)
		field = [|
			private $Disposed as bool
		|]
		field.LexicalInfo = LexicalInfo
		field.Documentation = \
			"""
			The flag which indicates whether
			this object has been disposed.
			"""
		DisposableType.Members.Add(field)
	
	protected def CreateProtectedDisposeMethod([required] cleanName as string):
	"""
	Creates the protected dispose method.
	Param cleanName: The name of the unmanaged dispose method.
		Or the empty string if none is specified.
	"""
		_manualDisposal = Ast.Block(LexicalInfo)
		_endDisposal = Ast.Block(LexicalInfo)
		_protectedDispose = [|
			protected virtual def Dispose():
				if $Disposed:
					return
				if disposing:
					$_manualDisposal
				$_endDisposal
				$Disposed = true
		|]
		disposingParam = CodeBuilder.CreateParameterDeclaration(
			0,
			'disposing',
			TypeSystemServices.BoolType,
			false)
		_protectedDispose.Parameters.Add(disposingParam)
		_protectedDispose.Documentation = \
			"""
			Dispose of any managed or unmanaged resources.
			Param disposing: Whether dispose was invoked.
			"""
		_protectedDispose.LexicalInfo = LexicalInfo
		if cleanName.Length != 0:
			cleanReference = Ast.ReferenceExpression(cleanName)
			unmanagedCall = [|
				$cleanReference()
			|]
			unmanagedCall.LexicalInfo = LexicalInfo
			_endDisposal.Add(unmanagedCall)
		DisposableType.Members.Add(_protectedDispose)
	
	protected def CreateDisposeMethod([required] name as string):
	"""
	Creates another name for Dispose.
	Param name: The name to call the method.
	"""
		nameReference = Ast.ReferenceExpression(name)
		dispose = [|
			final def $nameReference():
				Dispose(true)
				System.GC.SuppressFinalize(self)
		|]
		dispose.LexicalInfo = LexicalInfo
		dispose.Documentation = \
			"""Dispose of any managed or unmanaged resources."""
		DisposableType.Members.Add(dispose)
	
	protected def CreateExplicitDisposeMethod():
	"""Explicitly implement Dispose."""
		dispose = [|
			final def System.IDisposable.Dispose():
				Dispose(true)
				System.GC.SuppressFinalize(self)
		|]
		dispose.LexicalInfo = LexicalInfo
		dispose.Documentation = \
			"""Dispose of any managed or unmanaged resources."""
		DisposableType.Members.Add(dispose)
	
	protected def CreateFinalizer():
	"""Creates the finalizer."""
		finalizer = [|
			def destructor():
				Dispose(false)
		|]
		finalizer.LexicalInfo = LexicalInfo
		finalizer.Documentation = \
			"""Clean up any unmanaged resources."""
		DisposableType.Members.Add(finalizer)
	
	protected def CreateDisposedObjectCheck(
		skipList as SCG.ICollection[of string]):
	"""
	Creates the disposed object check for all methods.
	Param skipList: A list of names to ignore.
	Remarks:
		All non-private members will check if the instance has been disposed.
		If true, ObjectDisposedException will be thrown.
	"""
		for typeMember in DisposableType.Members:
			if not typeMember.IsStatic and \
				not typeMember.IsPrivate and \
				not typeMember.IsAbstract:
				if typeMember.NodeType == Ast.NodeType.Method:
					method = typeMember as Ast.Method
					if skipList.Contains(method.Name):
						continue
					CreateDisposedObjectCheck(method)
				elif typeMember.NodeType == Ast.NodeType.Property:
					property = typeMember as Ast.Property
					if property.Getter is not null:
						CreateDisposedObjectCheck(property.Getter)
					if property.Setter is not null:
						CreateDisposedObjectCheck(property.Setter)
	
	protected def CreateDisposedObjectCheck([required] method as Ast.Method):
	"""
	Creates the disposed object check.
	Param method: The method to add the disposed object check to.
	Remarks:
		Check if the instance has been disposed	of.  If true,
		ObjectDisposedException will be thrown.
	"""
		body = method.Body
		typeName = DisposableType.Name
		method.Body = [|
			if $Disposed:
				raise System.ObjectDisposedException($typeName)
			$body
		|]
		method.Body.LexicalInfo = LexicalInfo
		
	protected def CreateDisposedToString():
	"""Creates disposed checks for all ToString methods."""
		for typeMember in DisposableType.Members:
			if not typeMember.IsStatic and \
				not typeMember.IsPrivate and \
				not typeMember.IsAbstract:
				if typeMember.NodeType == Ast.NodeType.Method:
					method = typeMember as Ast.Method
					if method.Name != "ToString":
						continue
					body = method.Body
					method.Body = [|
						if $Disposed:
							return "[Disposed: ${GetType()}]"
						$body
					|]
					method.Body.LexicalInfo = LexicalInfo
	
	protected def GenerateDisposeCascade():
	"""
	Adjusts the protected dispose method body
	to include required functionality.
	"""
		disposableTag = TypeSystemServices.Map(IDisposable)
		ignoreDisposalTag = TypeSystemServices.Map(IgnoreDisposalAttribute)
		ignoreTag = TypeSystemServices.GetDefaultConstructor(ignoreDisposalTag)
		localDispose as Ast.ReferenceExpression = null
		for typeMember in DisposableType.Members:
			if typeMember.NodeType != Ast.NodeType.Field or \
				typeMember.IsStatic:
				continue
			field = typeMember as Ast.Field
			if field.Attributes.ContainsEntity(ignoreTag):
				continue
			fieldTypeTag = \
				TypeSystemServices.GetEntity(field.Type) as IType
			if fieldTypeTag.IsValueType:
				continue
			if not fieldTypeTag.IsSubclassOf(disposableTag):
				fieldTag = \
					TypeSystemServices.GetEntity(field) as IField
				fieldReference = CodeBuilder.CreateReference(fieldTag)
				nullAssignment = CodeBuilder.CreateAssignment(
					fieldReference,
					CodeBuilder.CreateNullLiteral())
				_manualDisposal.Add(nullAssignment)
			else:
				fieldTag = \
					TypeSystemServices.GetEntity(field) as IField
				fieldReference = CodeBuilder.CreateReference(fieldTag)
				condition = CodeBuilder.CreateBoundBinaryExpression(
					fieldTypeTag,
					Ast.BinaryOperatorType.Inequality,
					fieldReference,
					CodeBuilder.CreateNullLiteral())
				ifStatement = Ast.IfStatement(
					LexicalInfo,
					condition,
					Ast.Block(LexicalInfo),
					null)
				if localDispose is null:
					localDisposeName = "__dispose${Context.AllocIndex()}"
					localDisposeField = CodeBuilder.DeclareTempLocal(
						_protectedDispose,
						disposableTag)
					localDispose = CodeBuilder.CreateLocalReference(
						localDisposeName,
						localDisposeField)
				
				localAssignment = CodeBuilder.CreateAssignment(
					localDispose.CloneNode(),
					fieldReference.CloneNode())
				nullAssignment = CodeBuilder.CreateAssignment(
					fieldReference.CloneNode(),
					CodeBuilder.CreateNullLiteral())
				disposeReference = TypeSystemServices.Map(
					typeof(IDisposable).GetMethod('Dispose'))
				disposeInvocation = CodeBuilder.CreateMethodInvocation(
					localDispose.CloneNode(),
					disposeReference)
				ifStatement.TrueBlock.Add(localAssignment)
				ifStatement.TrueBlock.Add(nullAssignment)
				ifStatement.TrueBlock.Add(disposeInvocation)
				_manualDisposal.Add(ifStatement)
	
	[property(Alias)]
	private _alias as Ast.ReferenceExpression
	"""An alias for the dispose method."""
	
	[property(SharedCleanup)]
	private _sharedCleanup as Ast.ReferenceExpression
	"""
	The method which cleans up unmanaged resources.
	Remarks: Always called, after field disposal.
	"""
	
	[property(ForceFinalizer)]
	private _forceFinalizer as Ast.BoolLiteralExpression
	"""Whether or not to create the ."""
	
	[getter(DisposableType, Protected: true)]
	private _disposableType as Ast.TypeDefinition
	"""The node this attribute is applied to."""
	
	[getter(Disposed, Protected: true)]
	private _disposed as Ast.ReferenceExpression
	"""The disposed field."""
	
	private _protectedDispose as Ast.Method
	"""The protected dispose method."""
	
	private _manualDisposal as Ast.Block
	"""Where to append manual disposal code."""
	
	private _endDisposal as Ast.Block
	"""The last part of the disposal method."""
	
	private static _disposeKey = object()
	"""A key used in the context hash to avoid duplication."""

	